第 6 章 集合引用类型
6.1 Object
与对象一样,在使用数组字面量表示法创建数组不会调用 Array 构造函数。另一种方式是使用对象字面量(object literal)表示法。在使用对象字面量表示法定义对象时,并不会实际调用 Object 构造函数。
在对象字面量表示法中,属性名可以是字符串或数值,数值属性会自动转换为字符串。
属性一般是通过点语法来存取的,这也是面向对象语言的惯例,但也可以使用中括号来存取属性。在使用中括号时,要在括号内使用属性名的字符串形式,也可以通过变量访问属性。另外,如果属性名中包含可能会导致语法错误的字符,或者包含关键字/保留字时,也可以使用中括号语法。
let person = {
name: 'LBJ辉',
age: 29,
5: true
}
console.log(person['name']) // 'LBJ辉'
console.log(person.name) // 'LBJ辉'
let propertyName = 'name'
console.log(person[propertyName]) // 'LBJ辉'
person['first name'] = 'LBJ辉'6.2 Array
6.2.1 创建数组
① 使用 Array 构造函数
let arr = new Array()
let colors = new Array(3) // 创建一个包含 3 个元素的数组
let names = new Array('Greg') // 创建一个只包含一个元素,即字符串"Greg"的数组在使用 Array 构造函数时,也可以省略 new 操作符。
② 使用数组字面量(array literal)表示法。
let colors = ['red', 'blue', 'green'] // 创建一个包含 3 个元素的数组
let names = [] // 创建一个空数组
let values = [1, 2] // 创建一个包含 2 个元素的数组与对象一样,在使用数组字面量表示法创建数组不会调用 Array 构造函数。
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()和 of()。from()用于将类数组结构转换为数组实例,而 of()用于将一组参数转换为数组实例。
Array.from()的第一个参数是一个类数组对象(arrayLike),即任何可迭代的结构,或者有一个 length 属性和可索引元素的结构。
// 字符串会被拆分为单字符数组
console.log(Array.from('Matt')) // [ 'M', 'a', 't', 't' ]
// 可以使用 from() 将集合和映射转换为一个新数组
const m = new Map().set(1, 2).set(3, 4)
const s = new Set().add(1).add(2).add(3).add(4)
console.log(Array.from(m)) // [ [ 1, 2 ], [ 3, 4 ] ]
console.log(Array.from(s)) // [ 1, 2, 3, 4 ]
// Array.from() 对现有数组执行浅复制
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1)
console.log(a1) // [ 1, 2, 3, 4 ]
console.log(a1 === a2) // false
// 可以使用任何可迭代对象
const iter = {
*[Symbol.iterator]() {
yield 1
yield 2
yield 3
yield 4
}
}
console.log(Array.from(iter)) // [ 1, 2, 3, 4 ]
// arguments 对象可以被轻松地转换为数组
function getArgsArray() {
return Array.from(arguments)
}
console.log(getArgsArray(1, 2, 3, 4)) // [ 1, 2, 3, 4 ]
// from() 也能转换带有必要属性的自定义对象
const arrayLikeObject = {
0: 1,
1: 2,
2: 3,
3: 4,
length: 4
}
console.log(Array.from(arrayLikeObject)) // [ 1, 2, 3, 4 ]
// from() 不会创建稀疏数组。如果 arrayLike 对象缺少一些索引属性,那么这些属性在新数组中将是 undefined
const a = [1, , 1]
console.log(Array.from(a)) // [ 1, undefined, 1 ]Array.from()还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像调用 Array.from().map()那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函数中 this 的值。
const a1 = [1, 2, 3, 4]
const a2 = Array.from(a1, (x) => x ** 2)
const a3 = Array.from(
a1,
function (x) {
return x ** this.exponent
},
{ exponent: 3 }
)
console.log(a2) // [ 1, 4, 9, 16 ]
console.log(a3) // [ 1, 8, 27, 64 ]Array.of()可以把一组参数转换为数组。这个方法用于替代在 ES6 之前常用的 Array.prototype.slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:
console.log(Array.of(1, 2, 3, 4)) // [ 1, 2, 3, 4 ]
console.log(Array.of(undefined)) // [ undefined ]6.2.2 数组空位
使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。
ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined,ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异。
const options = [1, , , , 5]
for (const option of options) {
console.log(option === undefined) // false true true true false
}
const a = Array.from([, , ,]) // 使用 ES6 的 Array.from() 创建的包含 3 个空位的数组
for (const val of a) {
console.log(val === undefined) // true true true
}
console.log(Array.of(...[, , ,])) // [ undefined, undefined, undefined ]
for (const [index, value] of options.entries()) {
console.log(value) // 1 undefined undefined undefined 5
}
// map() 会跳过空位置
console.log(options.map(() => 6)) // [6, undefined, undefined, undefined, 6]
// join() 视空位置为空字符串
console.log(options.join('-')) // '1----5'6.2.3 数组索引
要取得或设置数组的值,需要使用中括号并提供相应值的数字索引。
在中括号中提供的索引表示要访问的值。如果索引小于数组包含的元素数,则返回存储在相应位置的元素。设置数组的值方法也是一样的,就是替换指定位置的值。如果把一个值设置给超过数组最大索引的索引 ,则数组长度会自动扩展到该索引值加 1。
数组中元素的数量保存在 length 属性中,这个属性始终返回 0 或大于 0 的值。数组 length 属性的独特之处在于,它不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。如果将 length 设置为大于数组元素数的值,则新添加的元素都将以 undefined 填充。
数组最多可以包含 4294967295 个元素,这对于大多数编程任务应该足够了。如果尝试添加更多项,则会导致抛出错误。以这个最大值作为初始值创建数组,可能导致脚本运行时间过长的错误。
6.2.4 检测数组
使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架(iframe),则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
为解决这个问题,ECMAScript 提供了 Array.isArray()方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
console.log(value instanceof Array)
console.log(Array.isArray(value))6.2.5 迭代器方法
在 ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()、values()和 entries()。keys()返回数组索引的迭代器,values()返回数组元素的迭代器,而 entries()返回索引/值对的迭代器:
const a = ['foo', 'bar', 'baz', 'qux']
// 因为这些方法都返回迭代器,所以可以将它们的内容
// 通过 Array.from() 直接转换为数组实例
console.log(Array.from(a.keys())) // [ 0, 1, 2, 3 ]
console.log(Array.from(a.values())) // [ 'foo', 'bar', 'baz', 'qux' ]
console.log(Array.from(a.entries())) // [ [ 0, 'foo' ], [ 1, 'bar' ], [ 2, 'baz' ], [ 3, 'qux' ] ]
const array1 = ['a', 'b', 'c']
const iterator = array1.values()
for (const value of iterator) {
console.log(value) // 'a', 'b', 'c'
}
console.log(Array.prototype.values === Array.prototype[Symbol.iterator]) // true
const array1 = ['a', 'b', 'c']
const iterator = array1.keys()
for (const key of iterator) {
console.log(key) // 0, 1, 2
}与 Object.keys() 只包含数组中实际存在的键不同,keys() 迭代器不会忽略缺失属性的键。
const arr = ['a', , 'c']
const sparseKeys = Object.keys(arr)
const denseKeys = [...arr.keys()]
console.log(sparseKeys) // [ '0', '2' ]
console.log(denseKeys) // [ 0, 1, 2 ]当在稀疏数组上使用时,entries() 方法迭代空槽,就像它们的值为 undefined 一样。
补充 entries() 方法详解
- 方法介绍
Object.entries()是 ES8(ECMAScript 2017)引入的一个方法,它用于将对象的所有可枚举属性转换成一个包含键值对的数组。每一个键值对都是一个数组,其中第一个元素是属性名,第二个元素是对应的属性值。
例如:
const obj = { a: 1, b: 2, c: 3 }
console.log(Object.entries(obj)) // [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]- 用法和返回值
Object.entries()返回一个二维数组,其中每个元素是一个数组,包含对象的键和值。该方法不会遍历不可枚举的属性,也不会遍历原型链上的属性,只会遍历对象自身的可枚举属性。
const person = {
name: 'John',
age: 30,
job: 'developer'
}
const entries = Object.entries(person)
console.log(entries) // [ [ 'name', 'John' ], [ 'age', 30 ], [ 'job', 'developer' ] ]- 数组遍历中的应用
entries()方法常常用于通过for...of循环遍历对象的键值对。这种方式使得代码更加简洁,且能够同时访问键和值:
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`)
}
// 'name: John'
// 'age: 30'
// 'job: developer'这种写法在处理复杂的对象时非常有用,尤其是在动态处理对象属性时。
- 在数组中的应用 除了用于对象,
Object.entries()还可以用于数组。对于数组,它将数组的索引和对应的值作为键值对返回:
const arr = ['apple', 'banana', 'cherry']
console.log(Object.entries(arr)) // [ [ '0', 'apple' ], [ '1', 'banana' ], [ '2', 'cherry' ] ]通过这种方式,你可以在遍历数组时,轻松获取索引和值。
- 应用场景
- 遍历对象属性:如果你想遍历一个对象的属性并进行一些操作,
Object.entries()提供了一种简便的方式。 - 处理动态数据:对于动态生成的对象,使用
entries()可以轻松地访问每个属性及其值。 - 转换对象格式:当需要将对象的数据转换为其他形式(如数组、映射)时,
entries()是一个非常有效的工具。
6.2.6 复制和填充方法
ES6 新增了两个方法:批量复制方法 copyWithin(),以及填充数组方法 fill()。
使用 fill()方法可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充的位置,它是可选的。如果不提供结束索引,则一直填充到数组末尾。负值索引从数组末尾开始计算。也可以将负索引想象成数组长度加上它得到的一个正索引:
const zeroes = [0, 0, 0, 0, 0]
// 用 5 填充整个数组
zeroes.fill(5)
console.log(zeroes) // [ 5, 5, 5, 5, 5 ]
zeroes.fill(0) // 重置
// 用 6 填充索引大于等于 3 的元素
zeroes.fill(6, 3)
console.log(zeroes) // [ 0, 0, 0, 6, 6 ]
zeroes.fill(0) // 重置
// 用 7 填充索引大于等于 1 且小于 3 的元素
zeroes.fill(7, 1, 3)
console.log(zeroes) // [ 0, 7, 7, 0, 0 ]
zeroes.fill(0) // 重置
// 用 8 填充索引大于等于 1 且小于 4 的元素
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4)
zeroes.fill(8, -4, -1)
console.log(zeroes) // [ 0, 8, 8, 8, 0 ]fill()静默忽略超出数组边界、零长度及方向相反的索引范围:
const zeroes = [0, 0, 0, 0, 0]
// 索引过低,忽略
zeroes.fill(1, -10, -6)
console.log(zeroes) // [ 0, 0, 0, 0, 0 ]
// 索引过高,忽略
zeroes.fill(1, 10, 15)
console.log(zeroes) // [ 0, 0, 0, 0, 0 ]
// 索引反向,忽略
zeroes.fill(2, 4, 2)
console.log(zeroes) // [ 0, 0, 0, 0, 0 ]
// 索引部分可用,填充可用部分
zeroes.fill(4, 3, 10)
console.log(zeroes) // [ 0, 0, 0, 0, 0 ]JS new Array.fill(new Array()) 创建二维数组 fill 方法的坑
const arr = new Array(5).fill(new Array(2).fill(0))
arr[0][0] = 1我们只想给 arr[0][0]赋值,但是每一行数组为 0 的下标元素的值全部改变了
原因:当 fill()的参数是一个引用类型的数据时,并不是将它的值填充到数组,而是将它的地址填充到数组,那么等于把这个数据的地址给了 arr 的每一项,相当于每一行都指向同一个数组地址,那么当你在操作任意一个位置的值时,所有行都会跟着变化。
解决方法
// for 循环填充行
let arr = new Array(5)
for (let i = 0; i < 5; i++) {
arr[i] = new Array(2).fill(0)
}
// Array.from()
const arr = Array.from(new Array(5).fill(), () => new Array(2).fill(0))
// 数组的 map 方法
let arr = new Array(5).fill(0).map((item) => new Array(2).fill(0))与 fill()不同,copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。开始索引和结束索引则与 fill()使用同样的计算方法:
let ints,
reset = () => (ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
reset()
// 从 ints 中复制索引 0 开始的内容,插入到索引 5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5)
console.log(ints) // [ 0, 1, 2, 3, 4, 0, 1, 2, 3, 4 ]
reset()
// 从 ints 中复制索引 5 开始的内容,插入到索引 0 开始的位置
ints.copyWithin(0, 5)
console.log(ints) // [ 5, 6, 7, 8, 9, 5, 6, 7, 8, 9 ]
reset()
// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3)
console.log(ints) // [ 0, 1, 2, 3, 0, 1, 2, 7, 8, 9 ]
reset()
// JavaScript 引擎在插值前会完整复制范围内的值
// 因此复制期间不存在重写的风险
ints.copyWithin(2, 0, 6)
console.log(ints) // [ 0, 1, 0, 1, 2, 3, 4, 5, 8, 9 ]
reset()
// 支持负索引值,与 fill() 相对于数组末尾计算正向索引的过程是一样的
ints.copyWithin(-4, -7, -3)
console.log(ints) // [ 0, 1, 2, 3, 4, 5, 3, 4, 5, 6 ]copyWithin()静默忽略超出数组边界、零长度及方向相反的索引范围:
let ints,
reset = () => (ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
reset()
// 索引过低,忽略
ints.copyWithin(1, -15, -12)
console.log(ints) // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
reset()
// 索引过高,忽略
ints.copyWithin(1, 12, 15)
console.log(ints) // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
reset()
// 索引反向,忽略
ints.copyWithin(2, 4, 2)
console.log(ints) // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
reset()
// 索引部分可用,复制、填充可用部分
ints.copyWithin(4, 7, 10)
console.log(ints) // [ 0, 1, 2, 3, 7, 8, 9, 7, 8, 9 ]copyWithin
copyWithin(target)
copyWithin(target, start)
copyWithin(target, start, end)target
序列开始替换的目标位置,以 0 为起始的下标表示,且将被转换为整数
- 负索引将从数组末尾开始计数——如果 target < 0,则实际是 target + array.length。
- 如果 target < -array.length,则使用 0。
- 如果 target >= array.length,则不会拷贝任何内容。
- 如果 target 位于 start 之后,则复制只会持续到 array.length 结束(换句话说,copyWithin() 永远不会扩展数组)。
start 可选
要复制的元素序列的起始位置,以 0 为起始的下标表示,且将被转换为整数
- 负索引将从数组末尾开始计数——如果 start < 0,则实际是 start + array.length。
- 如果省略 start 或 start < -array.length,则默认为 0。
- 如果 start >= array.length,则不会拷贝任何内容。
end 可选
要复制的元素序列的结束位置,以 0 为起始的下标表示,且将被转换为整数。copyWithin 将会拷贝到该位置,但不包括 end 这个位置的元素。
- 负索引将从数组末尾开始计数——如果 end < 0,则实际是 end + array.length。
- 如果 end < -array.length,则使用 0。
- 如果省略 end 或 end >= array.length,则默认为 array.length,这将导致直到数组末尾的所有元素都被复制。
- 如果 end 位于 start 之前,则不会拷贝任何内容。
6.2.7 转换方法
valueOf()返回的还是数组本身。而 toString()返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串。
let colors = ['red', 'blue', 'green'] // 创建一个包含 3 个字符串的数组
console.log(colors.toString()) // 'red,blue,green'
console.log(colors.valueOf()) // [ 'red', 'blue', 'green' ]
console.log(colors) // [ 'red', 'blue', 'green' ]toLocaleString()方法也可能返回跟 toString()和 valueOf()相同的结果,但也不一定。在调用数组的 toLocaleString()方法时,会得到一个逗号分隔的数组值的字符串。它与另外两个方法唯一的区别是,为了得到最终的字符串,会调用数组每个值的 toLocaleString()方法,而不是 toString()方法。
let person1 = {
toLocaleString() {
return 'Nikolaos'
},
toString() {
return 'Nicholas'
}
}
let person2 = {
toLocaleString() {
return 'Grigorios'
},
toString() {
return 'Greg'
}
}
let people = [person1, person2]
console.log(people.toString()) // 'Nicholas,Greg'
console.log(people.toLocaleString()) // 'Nikolaos,Grigorios'toLocaleString
toLocaleString()
toLocaleString(locales)
toLocaleString(locales, options)
const array1 = [1, 'a', new Date('21 Dec 1997 14:12:00 UTC')]
const localeString = array1.toLocaleString('en', { timeZone: 'UTC' })
console.log(localeString) // '1,a,12/21/1997, 2:12:00 PM'如果一个元素是 undefined、null,它会被转换为空字符串,而不是 "null" 或者 "undefined"。
当用于稀疏数组时,toLocaleString() 方法迭代时会把空槽当作 undefined 一样处理它。
toLocaleString() 方法是通用的。它只期望 this 值具有 length 属性和整数键属性。
join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。
6.2.8 栈方法
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。
pop()方法则用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项。
6.2.9 队列方法
shift()会删除数组的第一项并返回它,然后数组长度减 1。如果数组为空则返回 undefined。
unshift()在数组开头添加任意多个值,然后返回新的数组长度。
6.2.10 排序方法
reverse()方法就是将数组元素反向排列。
默认情况下,sort()会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。为此,sort()会在每一项上调用 String()转型函数,然后比较字符串,按照它们的 UTF-16 码元值升序排序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序。
sort()方法可以接收一个比较函数,用于判断哪个值应该排在前面。比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值。
sort() 方法保留空槽。如果源数组是稀疏的,则空槽会被移动到数组的末尾,并始终排在所有 undefined 元素的后面。
reverse()和 sort()都返回调用它们的数组的引用。
如果想要不改变原数组的排序方法,可以使用 toSorted()。
6.2.11 操作方法
concat()方法可以在现有数组全部元素基础上创建一个新数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果传入一个或多个数组,则 concat()会把这些数组的每一项都添加到结果数组。如果参数不是数组,则直接把它们添加到结果数组末尾。如果任何源数组是稀疏数组,concat() 方法会保留空槽。
let colors = ['red', 'green', 'blue']
let colors2 = colors.concat('yellow', ['black', 'brown'])
let colors3 = colors.concat('yellow', ['black', ['brown']])
console.log(colors) // [ 'red', 'green', 'blue' ]
console.log(colors2) // [ 'red', 'green', 'blue', 'yellow', 'black', 'brown' ]
console.log(colors3) // [ 'red', 'green', 'blue', 'yellow', 'black', [ 'brown' ] ]打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊的符号:Symbol.isConcat-Spreadable。这个符号能够阻止 concat()打平参数数组。相反,把这个值设置为 true 可以强制打平类数组对象:
let colors = ['red', 'green', 'blue']
let newColors = ['black', 'brown']
let moreNewColors = {
[Symbol.isConcatSpreadable]: true,
length: 2,
0: 'pink',
1: 'cyan'
}
newColors[Symbol.isConcatSpreadable] = false
// 强制不打平数组
let colors2 = colors.concat('yellow', newColors)
// 强制打平类数组对象
let colors3 = colors.concat(moreNewColors)
console.log(colors) // [ 'red', 'green', 'blue' ]
console.log(colors2) // [ 'red', 'green', 'blue', 'yellow', [ 'black', 'brown', Symbol(Symbol.isConcatSpreadable): false ]]
console.log(colors3) // [ 'red', 'green', 'blue', 'pink', 'cyan' ]slice()用于创建一个包含原有数组中一个或多个元素的新数组。slice()方法可以接收一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则 slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则 slice()返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。记住,这个操作不影响原始数组。如果 slice()的参数有负值,那么就以数值长度加上这个负值的结果确定位置。slice() 方法会保留空槽。如果被切片的部分是稀疏的,则返回的数组也是稀疏的。
let colors = ['red', 'green', 'blue', 'yellow', 'purple']
console.log(colors.slice(1)) // ['green', 'blue', 'yellow', 'purple']
console.log(colors) // ['red', 'green', 'blue', 'yellow', 'purple']
console.log(colors.slice(1, 4)) // ['green', 'blue', 'yellow']splice()的主要目的是在数组中间插入元素,但有 3 种不同的方式使用这个方法。
❑ 删除。需要给 splice()传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。可以从数组中删除任意多个元素,比如 splice(0, 2)会删除前两个元素。
❑ 插入。需要给 splice()传 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。第三个参数之后还可以传第四个、第五个参数,乃至任意多个要插入的元素。
❑ 替换。splice()在删除元素的同时可以在指定位置插入新元素,同样要传入 3 个参数:开始位置、要删除元素的数量和要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量一致。
let colors = ['red', 'green', 'blue']
let removed = colors.splice(0, 1) // 删除第一项
console.log(colors) // [ 'green', 'blue' ]
console.log(removed) // [ 'red' ],只有一个元素的数组
removed = colors.splice(1, 0, 'yellow', 'orange') // 在位置 1 插入两个元素
console.log(colors) // [ 'green', 'yellow', 'orange', 'blue' ]
console.log(removed) // [] 空数组
removed = colors.splice(1, 1, 'red', 'purple') // 插入两个值,删除一个元素
console.log(colors) // [ 'green', 'red', 'purple', 'orange', 'blue' ]
console.log(removed) // [ 'yellow' ],只有一个元素的数组要创建一个删除和/或替换部分内容而不改变原数组的新数组,请使用 toSpliced()。
6.2.12 搜索和位置方法
1.严格相等
ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()、lastIndexOf()和 includes()。这些方法都接收两个参数:要查找的元素(searchElement)和一个可选的起始搜索位置。indexOf()和 includes()方法从数组前头(第一项)开始向后搜索,而 lastIndexOf()从数组末尾(最后一项)开始向前搜索。
indexOf()和 lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回 -1。includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。
indexOf() 和 lastIndexOf() 使用严格相等(与 === 运算符使用的算法相同)将 searchElement 与数组中的元素进行比较。NaN 值永远不会被比较为相等,因此当 searchElement 为 NaN 时总是返回 -1。会跳过稀疏数组中的空槽。
includes() 方法使用零值相等(零值相等与严格相等的区别在于其将 NaN 视作是相等的,与同值相等的区别在于其将 -0 和 0 视作相等的。)算法将 searchElement 与数组中的元素进行比较。0 值都被认为是相等的,不管符号是什么。(即 -0 等于 0),但 false 不被认为与 0 相同。NaN 可以被正确搜索到。当在稀疏数组上使用时,includes() 方法迭代空槽,就像它们的值是 undefined 一样。
2.断言函数
ECMAScript 也允许按照定义的断言函数搜索数组,每个索引都会调用这个函数。断言函数的返回值决定了相应索引的元素是否被认为匹配。
断言函数接收 3 个参数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()和 findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回第一个匹配的元素,findIndex()返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数,用于指定断言函数内部 this 的值。
find
find() 不会改变被调用的数组,但是提供给 callbackFn 的函数可能会改变它。但需要注意的是,在第一次调用 callbackFn 之前,数组的长度会被保存。因此:
- 当调用
find()时,callbackFn不会访问超出数组初始长度的任何元素。 - 对已经访问过的索引的更改不会导致再次在这些元素上调用
callbackFn。 - 如果
callbackFn改变了数组中已存在但尚未被访问的元素,则传递给callbackFn的该元素的值将是该元素在被访问时的值。被删除的元素被视为undefined。
const people = [
{
name: 'Matt',
age: 27
},
{
name: 'Nicholas',
age: 29
}
]
console.log(people.find((element, index, array) => element.age < 28)) // { name: 'Matt', age: 27 }
console.log(people.findIndex((element, index, array) => element.age < 28)) // 0findLast() 方法反向迭代数组,并返回满足提供的测试函数的第一个元素的值。如果没有找到对应元素,则返回 undefined。
findLastIndex() 方法反向迭代数组,并返回满足所提供的测试函数的第一个元素的索引。若没有找到对应元素,则返回 -1。
如何检测一个数组中是否包含某一个元素?
- indexOf 返回元素下标,没有返回 -1
- find 查找并返回目标元素,没有 undefined
- findIndex 查找并返回目标元素下标,没有返回 -1
- some 查找数组中是否有符合条件的元素,返回 true/false
- includes 返回数组是否包含指定的元素,返回 true/false
6.2.13 迭代方法
ECMAScript 为数组定义了 5 个迭代方法。每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影响方法的返回值。数组的 5 个迭代方法如下。
❑ every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
❑ filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
❑ forEach():对数组每一项都运行传入的函数,没有返回值。
❑ map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
❑ some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。
使用 every()方法时,有一种特殊情况需要注意:如果调用 every()方法的是一个空数组,则返回 true。
filter
- 当开始调用
filter()时,callbackFn将不会访问超出数组初始长度的任何元素。 - 对已访问索引的更改不会导致再次在这些元素上调用
callbackFn。 - 如果数组中一个现有的、尚未访问的元素被
callbackFn更改,则它传递给callbackFn的值将是该元素被修改后的值。被删除的元素则不会被访问。
forEach
forEach() 不会改变其调用的数组,但是,作为 callbackFn 的函数可以更改数组。请注意,在第一次调用 callbackFn 之前,数组的长度已经被保存。因此:
- 当调用
forEach()时,callbackFn不会访问超出数组初始长度的任何元素。 - 已经访问过的索引的更改不会导致
callbackFn再次调用它们。 - 如果
callbackFn更改了数组中已经存在但尚未访问的元素,则传递给callbackFn的值将是在访问该元素时的值。已经被删除的元素不会被访问。
除非抛出异常,否则没有办法停止或中断 forEach() 循环。
手写 forEach 实现
Array.prototype.myForEach = async function (callback, thisArg) {
const _arr = this,
_isArray = Array.isArray(_arr),
_thisArg = thisArg ? Object(thisArg) : globalThis
if (!_isArray) {
throw new TypeError('The caller of myForEach must be the type an array')
}
for (let i = 0; i < _arr.length; i++) {
await callback.call(_thisArg, _arr[i], i, _arr)
}
}使用 some()方法时,当调用数组为空数组时,返回结果为 false。
6.2.14 归并方法
reduce()方法从数组第一项开始遍历到最后一项。而 reduceRight()从最后一项开始遍历至第一项。
两个方法都接收两个参数:对每一项都会运行的归并函数,以及可选的以之为归并起点的初始值。传给 reduce()和 reduceRight()的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。
数组转成嵌套对象 ["a","b","c","d"] => {a: {b: {c: {d: null}}}}
const fun = (arr) => {
return arr.reduceRight((acc, cur) => {
return {
[cur]: acc
}
}, null)
}6.3 定型数组
TypedArray 是用于高效处理二进制数据的工具,常用于 WebGL、音频处理、网络协议等场景。
6.3.1 历史
6.3.2 ArrayBuffer
ArrayBuffer 是所有定型数组及视图引用的基本单位。
ArrayBuffer()是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间。
const buf = new ArrayBuffer(16) // 在内存中分配 16 字节
console.log(buf.byteLength) // 16ArrayBuffer 一经创建就不能再调整大小。不过,可以使用 slice()复制其全部或部分到一个新实例中:
const buf1 = new ArrayBuffer(16)
const buf2 = buf1.slice(4, 12)
console.log(buf2.byteLength) // 8ArrayBuffer 某种程度上类似于 C++的 malloc(),但也有几个明显的区别。
❑ malloc()在分配失败时会返回一个 null 指针。ArrayBuffer在分配失败时会抛出错误。
❑ malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制。ArrayBuffer分配的内存不能超过Number.MAX_SAFE_INTEGER(253-1)字节。
❑ malloc()调用成功不会初始化实际的地址。声明 ArrayBuffer 则会将所有二进制位初始化为 0。
❑ 通过 malloc()分配的堆内存除非调用 free()或程序退出,否则系统不能再使用。而通过声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不用手动释放。
不能仅通过对 ArrayBuffer 的引用就读取或写入其内容。要读取或写入 ArrayBuffer,就必须通过视图。视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据。
6.3.3 DataView
第一种允许你读写 ArrayBuffer 的视图是 DataView。这个视图专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。DataView 对缓冲内容没有任何预设,也不能迭代。必须在对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例。这个实例可以使用全部或部分 ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置。DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
const buf = new ArrayBuffer(16)
// DataView 默认使用整个 ArrayBuffer
const fullDataView = new DataView(buf)
console.log(fullDataView.byteOffset) // 0
console.log(fullDataView.byteLength) // 16
console.log(fullDataView.buffer === buf) // true
// 构造函数接收一个可选的字节偏移量和字节长度
// byteOffset=0 表示视图从缓冲起点开始
// byteLength=8 限制视图为前 8 个字节
const firstHalfDataView = new DataView(buf, 0, 8)
console.log(firstHalfDataView.byteOffset) // 0
console.log(firstHalfDataView.byteLength) // 8
console.log(firstHalfDataView.buffer === buf) // true
// 如果不指定,则 DataView 会使用剩余的缓冲
// byteOffset=8 表示视图从缓冲的第 9 个字节开始
// byteLength 未指定,默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8)
console.log(secondHalfDataView.byteOffset) // 8
console.log(secondHalfDataView.byteLength) // 8
console.log(secondHalfDataView.buffer === buf) // true要通过 DataView 读取缓冲,还需要几个组件。
❑ 首先是要读或写的字节偏移量。可以看成 DataView 中的某种“地址”。
❑ DataView 应该使用 ElementType 来实现 JavaScript 的 Number 类型到缓冲内二进制格式的转换。
❑ 最后是内存中值的字节序。默认为大端字节序。
1.ElementType
DataView 对存储在缓冲内的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个 ElementType,然后 DataView 就会忠实地为读、写而完成相应的转换。
2.字节序
DataView 访问器(accessor)提供了对如何访问数据的明确控制,而不管执行代码的计算机的字节序如何。
DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序。
const littleEndian = (() => {
const buffer = new ArrayBuffer(2)
new DataView(buffer).setInt16(0, 256, true /* 小端对齐 */)
// Int16Array 使用平台的字节序。
return new Int16Array(buffer)[0] === 256
})()
console.log(littleEndian) // true 或 false3.边界情形
DataView 完成读、写操作的前提是必须有充足的缓冲区,否则就会抛出 RangeError:
const buf = new ArrayBuffer(6)
const view = new DataView(buf)
// 尝试读取部分超出缓冲范围的值
view.getInt32(4) // RangeError
// 尝试读取超出缓冲范围的值
view.getInt32(8) // RangeError
// 尝试读取超出缓冲范围的值
view.getInt32(-1) // RangeError
// 尝试写入超出缓冲范围的值
view.setInt32(4, 123) // RangeErrorDataView 在写入缓冲里会尽最大努力把一个值转换为适当的类型,后备为 0。如果无法转换,则抛出错误:
const buf = new ArrayBuffer(1)
const view = new DataView(buf)
view.setInt8(0, 1.5)
console.log(view.getInt8(0)) // 1
view.setInt8(0, [4])
console.log(view.getInt8(0)) // 4
view.setInt8(0, 'f')
console.log(view.getInt8(0)) // 0
view.setInt8(0, Symbol()) // TypeError6.3.4 定型数组
定型数组是另一种形式的 ArrayBuffer 视图。虽然概念上与 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。
创建定型数组的方式包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外,通过<ElementType>.from()和<ElementType>.of()也可以创建定型数组:
// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12)
// 创建一个引用该缓冲的 Int32Array:每个元素是 32 位有符号整数,即 4 字节。
const ints = new Int32Array(buf)
// 这个定型数组知道自己的每个元素需要 4 字节
// 因此长度为 3
console.log(ints.length) // 3
// 创建一个包含 6 个 32 位整数的定型数组
const ints2 = new Int32Array(6)
// 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
console.log(ints2.length) // 6
// 类似 DataView,定型数组也有一个指向关联缓冲的引用
console.log(ints2.buffer.byteLength) // 24
// 创建一个包含 [2, 4, 6, 8] 的 Int32Array,自动创建一个新的 ArrayBuffer(4 元素 × 4 字节 = 16 字节)。
const ints3 = new Int32Array([2, 4, 6, 8])
console.log(ints3.length) // 4
console.log(ints3.buffer.byteLength) // 16
console.log(ints3[2]) // 6
// 通过复制 ints3 的值创建一个 Int16Array
const ints4 = new Int16Array(ints3)
// 这个新类型数组会分配自己的缓冲
// 对应索引的每个值会相应地转换为新格式
console.log(ints4.length) // 4
console.log(ints4.buffer.byteLength) // 8
console.log(ints4[2]) // 6
// 基于普通数组来创建一个 Int16Array
const ints5 = Int16Array.from([3, 5, 7, 9])
console.log(ints5.length) // 4
console.log(ints5.buffer.byteLength) // 8
console.log(ints5[2]) // 7
// 基于传入的参数创建一个 Float32Array
const floats = Float32Array.of(3.14, 2.718, 1.618)
console.log(floats.length) // 3
console.log(floats.buffer.byteLength) // 12
console.log(floats[2]) // 1.6180000305175781| 操作 | 是否共享 buffer | 说明 |
|---|---|---|
new TypedArray(ArrayBuffer) | ✅ 共享 | 创建视图 |
new TypedArray(length) | ❌ 新建 | 自动分配新 buffer |
new TypedArray(typedArray) | ❌ 复制值 | 创建新 buffer,逐个转换值 |
new TypedArray(jsArray) | ❌ 复制值 | 创建新 buffer |
TypedArray.from(...) | ❌ 复制值 | 类似构造函数传数组 |
TypedArray.of(...) | ❌ 新建 | 直接从参数列表创建 |
定型数组的构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小:
console.log(Int16Array.BYTES_PER_ELEMENT) // 2
console.log(Int32Array.BYTES_PER_ELEMENT) // 4
const ints = new Int32Array(1),
floats = new Float64Array(1)
console.log(ints.BYTES_PER_ELEMENT) // 4
console.log(floats.BYTES_PER_ELEMENT) // 8如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:
const ints = new Int32Array(4)
console.log(ints[0]) // 0
console.log(ints[1]) // 0
console.log(ints[2]) // 0
console.log(ints[3]) // 01.定型数组行为
❑ findIndex()
❑ forEach()
❑ indexOf()
❑ join()
❑ keys()
❑ lastIndexOf()
❑ length
❑ map()
❑ reduce()
❑ reduceRight()
❑ reverse()
❑ slice()
❑ some()
❑ sort()
❑ toLocaleString()
❑ toString()
❑ values()
其中,返回新数组的方法也会返回包含同样元素类型(element type)的新定型数组:
const ints = new Int16Array([1, 2, 3])
const doubleints = ints.map((x) => 2 * x)
console.log(doubleints instanceof Int16Array) // true定型数组有一个 Symbol.iterator 符号属性,因此可以通过 for...of 循环和扩展操作符来操作:
const ints = new Int16Array([1, 2, 3])
for (const int of ints) {
console.log(int) // 1, 2, 3
}
console.log(Math.max(...ints)) // 32.合并、复制和修改定型数组
定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。因此,下列方法不适用于定型数组:
❑ concat()
❑ pop()
❑ push()
❑ shift()
❑ splice()
❑ unshift()
定型数组也提供了两个新方法,可以快速向外或向内复制数据:set()和 subarray()。
set()从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置:
// 创建长度为 8 的 int16 数组
const container = new Int16Array(8)
// 把定型数组复制为前 4 个值
// 偏移量默认为索引 0
container.set(Int8Array.of(1, 2, 3, 4))
console.log(container) // Int16Array { 0: 1, 1: 2, 2: 3, 3: 4, 4: 0, 5: 0, 6: 0, 7: 0 }
// 把普通数组复制为后 4 个值
// 偏移量 4 表示从索引 4 开始插入
container.set([5, 6, 7, 8], 4)
console.log(container) // Int16Array { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8 }
// 溢出会抛出错误
container.set([5, 6, 7, 8], 7) // offset is out of boundssubarray()执行与 set()相反的操作,它会基于从原始定型数组中复制的值返回一个新定型数组。复制值时的开始索引和结束索引是可选的:
const source = Int16Array.of(2, 4, 6, 8)
// 把整个数组复制为一个同类型的新数组
const fullCopy = source.subarray()
console.log(fullCopy) // Int16Array { 0: 2, 1: 4, 2: 6, 3: 8 }
// 从索引 2 开始复制数组
const halfCopy = source.subarray(2)
console.log(halfCopy) // Int16Array { 0: 6, 1: 8 }
// 从索引 1 开始复制到索引 3
const partialCopy = source.subarray(1, 3)
console.log(partialCopy) // Int16Array { 0: 4, 1: 6 }3.下溢和上溢
6.4 Map
6.4.1 基本 API
使用 new 关键字和 Map 构造函数可以创建一个空映射:
const m = new Map()如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:
// 使用嵌套数组初始化映射
const m1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
console.log(m1.size) // 3
// 使用自定义迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function* () {
yield ['key1', 'val1']
yield ['key2', 'val2']
yield ['key3', 'val3']
}
})
console.log(m2.size) // 3
// 映射期待的键/值对,无论是否提供
const m3 = new Map([[]])
console.log(m3.has(undefined)) // true
console.log(m3.get(undefined)) // undefined初始化之后,可以使用 set()方法再添加键/值对。另外,可以使用 get()和 has()进行查询,可以通过 size 属性获取映射中的键/值对的数量,还可以使用 delete()和 clear()删除值。
const m = new Map()
console.log(m.has('firstName')) // false
console.log(m.get('firstName')) // undefined
console.log(m.size) // 0
m.set('firstName', 'Matt').set('lastName', 'Frisbie')
console.log(m.has('firstName')) // true
console.log(m.get('firstName')) // 'Matt'
console.log(m.size) // 2
m.delete('firstName') // 只删除这一个键/值对
console.log(m.has('firstName')) // false
console.log(m.has('lastName')) // true
console.log(m.size) // 1
m.clear() // 清除这个映射实例中的所有键/值对
console.log(m.has('firstName')) // false
console.log(m.has('lastName')) // false
console.log(m.size) // 0set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明。
const m = new Map().set('key1', 'val1')
m.set('key2', 'val2').set('key3', 'val3')
console.log(m.size) // 3与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。
const m = new Map()
const functionKey = function () {}
const symbolKey = Symbol()
const objectKey = new Object()
m.set(functionKey, 'functionValue')
m.set(symbolKey, 'symbolValue')
m.set(objectKey, 'objectValue')
console.log(m.get(functionKey)) // functionValue
console.log(m.get(symbolKey)) // symbolValue
console.log(m.get(objectKey)) // objectValue
// SameValueZero 比较意味着独立实例不冲突
console.log(m.get(function () {})) // undefined与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变:
const m = new Map()
const objKey = {},
objVal = {},
arrKey = [],
arrVal = []
m.set(objKey, objVal)
m.set(arrKey, arrVal)
objKey.foo = 'foo'
objVal.bar = 'bar'
arrKey.push('foo')
arrVal.push('bar')
console.log(m.get(objKey)) // { bar: 'bar' }
console.log(m.get(arrKey)) // [ 'bar' ]SameValueZero 比较也可能导致意想不到的冲突:
const m = new Map()
const a = 0 / '', // NaN
b = 0 / '', // NaN
pz = +0,
nz = -0
console.log(a === b) // false
console.log(pz === nz) // true
m.set(a, 'foo')
m.set(pz, 'bar')
console.log(m.get(b)) // foo
console.log(m.get(nz)) // bar补充
Map.groupBy() 可以让可枚举对象,根据某个键进行自动分组。
Map.groupBy(items, callbackFn)items 将被分组的可迭代对象。
callbackFn(element, index) 为可迭代对象中的每个元素执行的函数。其返回值会被作为键,用来指向分组后的数组项。
const inventory = [
{ name: 'asparagus', type: 'vegetables', quantity: 9 },
{ name: 'bananas', type: 'fruit', quantity: 5 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherries', type: 'fruit', quantity: 12 },
{ name: 'fish', type: 'meat', quantity: 22 }
]
const restock = { restock: true }
const sufficient = { restock: false }
const result = Map.groupBy(inventory, ({ quantity }) => (quantity < 6 ? restock : sufficient))
console.log(result.get(restock)) // [{ name: 'bananas', type: 'fruit', quantity: 5 }]6.4.2 顺序与迭代
与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以通过 entries()方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
console.log(m.entries === m[Symbol.iterator]) // true
for (let pair of m.entries()) {
console.log(pair) // [ 'key1', 'val1' ] [ 'key2', 'val2' ] [ 'key3', 'val3' ]
}
for (let pair of m[Symbol.iterator]()) {
console.log(pair) // [ 'key1', 'val1' ] [ 'key2', 'val2' ] [ 'key3', 'val3' ]
}因为 entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
console.log([...m]) // [ [ 'key1', 'val1' ], [ 'key2', 'val2' ], [ 'key3', 'val3' ] ]如果不使用迭代器,而是使用回调方式,则可以调用映射的 forEach(callback,opt_thisArg)方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调内部 this 的值:
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
m.forEach((val, key) => console.log(`${key} -> ${val}`)) // key1-> val1 key2-> val2 key3-> val3keys()和 values()分别返回以插入顺序生成键和值的迭代器:
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
for (let key of m.keys()) {
console.log(key) // 'key1' 'key2' 'key3'
}
for (let key of m.values()) {
console.log(key) // 'val1' 'val2' 'val3'
}键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份:
const m1 = new Map([['key1', 'val1']])
// 作为键的字符串原始值是不能修改的
for (let key of m1.keys()) {
key = 'newKey'
console.log(key) // 'newKey'
console.log(m1.get('key1')) // 'val1'
}
const keyObj = { id: 1 }
const m = new Map([[keyObj, 'val1']])
// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值
for (let key of m.keys()) {
key.id = 'newKey'
console.log(key) // { id: 'newKey' }
console.log(m.get(keyObj)) // 'val1'
}
console.log(keyObj) // { id: 'newKey' }6.4.3 选择 Object 还是 Map
1.内存占用
Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。
2.插入性能
向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。
3.查找速度
与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。
4.删除性能
使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。
object 和 map 有什么相同点和不同点
- 创建方式的区别
- 通过字面量创建 Object、通过构造函数创建 Object
- 通过构造函数创建 Map
- key 的类型不同
- Object:对象的键是字符串或者 Symbol
- Map:Map 可以使用任何类型的值作为键,包括对象、函数、原始值等。
- key 的顺序
- Object:key 的顺序与插入顺序无关
- Map:key 的顺序就是插入的顺序
6.5 WeakMap
ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。
6.5.1 基本 API
可以使用 new 关键字实例化一个空的 WeakMap:
const wm = new WeakMap()弱映射中的键只能是 Object 或者继承自 Object 的类型或非全局注册的符号,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。
如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:
const key1 = { id: 1 },
key2 = { id: 2 },
key3 = { id: 3 }
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
[key1, 'val1'],
[key2, 'val2'],
[key3, 'val3']
])
console.log(wm1.get(key1)) // 'val1'
console.log(wm1.get(key2)) // 'val2'
console.log(wm1.get(key3)) // 'val3'
// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
[key1, 'val1'],
['BADKEY', 'val2'],
[key3, 'val3']
])
// TypeError: Invalid value used as WeakMap key
// 原始值可以先包装成对象再用作键
const stringKey = new String('key1')
const wm3 = new WeakMap([[stringKey, 'val1']])
console.log(wm3.get(stringKey)) // "val1"初始化之后可以使用 set()再添加键/值对,可以使用 get()和 has()查询,还可以使用 delete()删除。set()方法返回弱映射实例,因此可以把多个操作连缀起来,包括初始化声明
6.5.2 弱键
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
6.5.3 不可迭代键
6.5.4 使用弱映射
1.私有变量
映射造就了在 JavaScript 中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
const wm = new WeakMap()
class User {
constructor(id) {
this.idProperty = Symbol('id')
this.setId(id)
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {}
privateMembers[property] = value
wm.set(this, privateMembers)
}
getPrivate(property) {
return wm.get(this)[property]
}
setId(id) {
this.setPrivate(this.idProperty, id)
}
getId() {
return this.getPrivate(this.idProperty)
}
}
const user = new User(123)
console.log(user.getId()) // 123
user.setId(456)
console.log(user.getId()) // 456
// 并不是真正私有的
console.log(wm.get(user)[user.idProperty]) // 456对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。为了避免这种访问,可以用一个闭包把 WeakMap 包装起来,这样就可以把弱映射与外界完全隔离开了:
const User = (() => {
const wm = new WeakMap()
class User {
constructor(id) {
this.idProperty = Symbol('id')
this.setId(id)
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {}
privateMembers[property] = value
wm.set(this, privateMembers)
}
getPrivate(property) {
return wm.get(this)[property]
}
setId(id) {
this.setPrivate(this.idProperty, id)
}
getId(id) {
return this.getPrivate(this.idProperty)
}
}
return User
})()
const user = new User(123)
console.log(user.getId()) // 123
user.setId(456)
console.log(user.getId()) // 456这样,拿不到弱映射中的健,也就无法取得弱映射中对应的值。虽然这防止了前面提到的访问,但整个代码也完全陷入了 ES6 之前的闭包私有变量模式。
2.DOM 节点元数据
因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中使用了常规的 Map:
const m = new Map()
const loginButton = document.querySelector('#login')
// 给这个节点关联一些元数据
m.set(loginButton, { disabled: true })假设在上面的代码执行后,页面被 JavaScript 改变了,原来的登录按钮从 DOM 树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。
如果这里使用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):
const wm = new WeakMap()
const loginButton = document.querySelector('#login')
// 给这个节点关联一些元数据
wm.set(loginButton, { disabled: true })6.6 Set
ECMAScript 6 新增的 Set 是一种新集合类型,为这门语言带来集合数据结构。Set 在很多方面都像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的。
6.6.1 基本 API
使用 new 关键字和 Set 构造函数可以创建一个空集合:
const m = new Set()如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素:
// 使用数组初始化集合
const s1 = new Set(['val1', 'val2', 'val3'])
console.log(s1.size) // 3
// 使用自定义迭代器初始化集合
const s2 = new Set({
[Symbol.iterator]: function* () {
yield 'val1'
yield 'val2'
yield 'val3'
}
})
console.log(s2.size) // 3初始化之后,可以使用 add()增加值,使用 has()查询,通过 size 取得元素数量,以及使用 delete()和 clear()删除元素:
const s = new Set()
console.log(s.has('Matt')) // false
console.log(s.size) // 0
s.add('Matt').add('Frisbie')
console.log(s.has('Matt')) // true
console.log(s.size) // 2
s.delete('Matt')
console.log(s.has('Matt')) // false
console.log(s.has('Frisbie')) // true
console.log(s.size) // 1
s.clear() // 销毁集合实例中的所有值
console.log(s.has('Matt')) // false
console.log(s.has('Frisbie')) // false
console.log(s.size) // 0add()返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化:
const s = new Set().add('val1')
s.add('val2').add('val3')
console.log(s.size) // 3与 Map 类似,Set 可以包含任何 JavaScript 数据类型作为值。集合也使用 SameValueZero 操作(ECMAScript 内部定义,无法在语言中使用),基本上相当于使用严格对象相等的标准来检查值的匹配性。与严格相等一样,用作值的对象和其他“集合”类型在自己的内容或属性被修改时也不会改变。
add()和delete()操作是幂等的。delete()返回一个布尔值,表示集合中是否存在要删除的值:
const s = new Set()
s.add('foo')
console.log(s.size) // 1
s.add('foo')
console.log(s.size) // 1
// 集合里有这个值
console.log(s.delete('foo')) // true
// 集合里没有这个值
console.log(s.delete('foo')) // false6.6.2 顺序与迭代
Set 会维护值插入时的顺序,因此支持按顺序迭代。
集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过 values()方法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values())取得这个迭代器:
const s = new Set(['val1', 'val2', 'val3'])
console.log(s.values === s[Symbol.iterator]) // true
console.log(s.keys === s[Symbol.iterator]) // true
for (let value of s.values()) {
console.log(value) // 'val1' 'val2' 'val3'
}
for (let value of s[Symbol.iterator]()) {
console.log(value) // 'val1' 'val2' 'val3'
}因为 values()是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组:
const s = new Set(['val1', 'val2', 'val3'])
console.log([...s]) // [ 'val1', 'val2', 'val3' ]如果不使用迭代器,而是使用回调方式,则可以调用集合的 forEach()方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调内部 this 的值:
const s = new Set(['val1', 'val2', 'val3'])
s.forEach((val, dupVal) => console.log(`${val} -> ${dupVal}`)) // val1-> val1 val2-> val2 val3-> val3修改集合中值的属性不会影响其作为集合值的身份:
const s1 = new Set(['val1'])
// 字符串原始值作为值不会被修改
for (let value of s1.values()) {
value = 'newVal'
console.log(value) // 'newVal'
console.log(s1.has('val1')) // true
}
const valObj = { id: 1 }
const s2 = new Set([valObj])
// 修改值对象的属性,但对象仍然存在于集合中
for (let value of s2.values()) {
value.id = 'newVal'
console.log(value) // { id: 'newVal' }
console.log(s2.has(valObj)) // true
}
console.log(valObj) // { id: 'newVal' }6.6.3 定义正式集合操作
❑ 某些 Set 操作是有关联性的,因此最好让实现的方法能支持处理任意多个集合实例。
❑ Set 保留插入顺序,所有方法返回的集合必须保证顺序。
❑ 尽可能高效地使用内存。扩展操作符的语法很简洁,但尽可能避免集合和数组间的相互转换能够节省对象初始化成本。
❑ 不要修改已有的集合实例。union(a, b)或 a.union(b)应该返回包含结果的新集合实例。
class XSet extends Set {
union(...sets) {
return XSet.union(this, ...sets)
}
intersection(...sets) {
return XSet.intersection(this, ...sets)
}
difference(set) {
return XSet.difference(this, set)
}
symmetricDifference(set) {
return XSet.symmetricDifference(this, set)
}
cartesianProduct(set) {
return XSet.cartesianProduct(this, set)
}
powerSet() {
return XSet.powerSet(this)
}
// 返回两个或更多集合的并集
static union(a, ...bSets) {
const unionSet = new XSet(a)
for (const b of bSets) {
for (const bValue of b) {
unionSet.add(bValue)
}
}
return unionSet
}
// 返回两个或更多集合的交集
static intersection(a, ...bSets) {
const intersectionSet = new XSet(a)
for (const aValue of intersectionSet) {
for (const b of bSets) {
if (!b.has(aValue)) {
intersectionSet.delete(aValue)
}
}
}
return intersectionSet
}
// 返回两个集合的差集
static difference(a, b) {
const differenceSet = new XSet(a)
for (const bValue of b) {
if (a.has(bValue)) {
differenceSet.delete(bValue)
}
}
return differenceSet
}
// 返回两个集合的对称差集
static symmetricDifference(a, b) {
// 按照定义,对称差集可以表达为
return a.union(b).difference(a.intersection(b))
}
// 返回两个集合(数组对形式)的笛卡儿积
// 必须返回数组集合,因为笛卡儿积可能包含相同值的对
static cartesianProduct(a, b) {
const cartesianProductSet = new XSet()
for (const aValue of a) {
for (const bValue of b) {
cartesianProductSet.add([aValue, bValue])
}
}
return cartesianProductSet
}
// 返回一个集合的幂集
static powerSet(a) {
const powerSet = new XSet().add(new XSet())
for (const aValue of a) {
for (const set of new XSet(powerSet)) {
powerSet.add(new XSet(set).add(aValue))
}
}
return powerSet
}
}7 种 JavaScript 中新的 Set 方法
- Intersection():寻找共同点
这个方法揭示了两个集合之间的共同元素。把它想象成一个维恩图,突出显示重叠区域。
const setA = new Set([1, 2, 3, 4])
const setB = new Set([3, 4, 5, 6])
const intersection = setA.intersection(setB)
// Expected output: Set {3, 4}- union():联合力量
union() 方法将两个集合中的独特元素合并为一个全新的集合。可以将其想象为合并两个组,每个组仅保留一个实例。
const setA = new Set([1, 2, 3])
const setB = new Set([3, 4, 5])
const unionSet = setA.union(setB)
// Expected output: Set {1, 2, 3, 4, 5}- difference():找出唯一性
此方法可精确定位第一组中存在但第二组中不存在的元素。想象一下从一组元素中减去另一组元素。
const setA = new Set([1, 2, 3])
const setB = new Set([3, 4, 5])
const differenceSetA = setA.difference(setB)
// Expected output: Set {1, 2}
const differenceSetB = setB.difference(setA)
// Expected output: Set {4, 5}- symmetricDifference():突出差异
此方法主要是为了强调两个集合之间的差异。它收集每个集合独有的元素,排除任何共享元素。
const setA = new Set([1, 2, 3])
const setB = new Set([3, 4, 5])
const symmetricDifferenceSetA = setA.symmetricDifference(setB)
// Expected output: Set {1, 2, 4, 5}
const symmetricDifferenceSetB = setB.symmetricDifference(setA)
// Expected output: Set {4, 5, 1, 2}- isSubsetOf():检查包含性
此方法确定一个集合的所有元素是否存在于另一个集合中。可以将其视为检查一个较小的盒子是否完全适合一个较大的盒子。
const setA = new Set([2, 3])
const setB = new Set([1, 2, 3, 4])
const isSubset = setA.isSubsetOf(setB)
// Expected output: true- isSupersetOf():逆关系
顾名思义,此方法是 isSubsetOf() 的逆方法。它检查一个集合是否完全包含另一个集合的所有元素。
const setA = new Set([1, 2, 3, 4])
const setB = new Set([2, 3])
const isSuperset = setA.isSupersetOf(setB)
// Expected output: true- isDisjointFrom():识别分离
此方法可帮助我们找出两个集合是否有任何共同元素。
const setA = new Set([1, 2])
const setB = new Set([3, 4])
const setC = new Set([4, 5])
const areDisjoint1 = setA.isDisjointFrom(setB)
// Expected output: true
const areDisjoint2 = setB.isDisjointFrom(setC)
// Expected output: false6.7 WeakSet
6.7.1 基本 API
可以使用 new 关键字实例化一个空的 WeakSet:
const ws = new WeakSet()弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError。
如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中:
const val1 = { id: 1 },
val2 = { id: 2 },
val3 = { id: 3 }
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3])
console.log(ws1.has(val1)) // true
console.log(ws1.has(val2)) // true
console.log(ws1.has(val3)) // true
// 初始化是全有或全无的操作
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, 'BADVAL', val3])
// TypeError: Invalid value used in WeakSet
typeof ws2
// ReferenceError: ws2 is not defined
// 原始值可以先包装成对象再用作值
const stringVal = new String('val1')
const ws3 = new WeakSet([stringVal])
console.log(ws3.has(stringVal)) // true初始化之后可以使用 add()再添加新值,可以使用 has()查询,还可以使用 delete()删除:
const ws = new WeakSet()
const val1 = { id: 1 },
val2 = { id: 2 }
console.log(ws.has(val1)) // false
ws.add(val1).add(val2)
console.log(ws.has(val1)) // true
console.log(ws.has(val2)) // true
ws.delete(val1) // 只删除这一个值
console.log(ws.has(val1)) // false
console.log(ws.has(val2)) // trueadd()方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明:
const val1 = { id: 1 },
val2 = { id: 2 },
val3 = { id: 3 }
const ws = new WeakSet().add(val1)
ws.add(val2).add(val3)
console.log(ws.has(val1)) // true
console.log(ws.has(val2)) // true
console.log(ws.has(val3)) // true6.7.2 弱值
6.7.3 不可迭代值
6.7.4 使用弱集合
相比于 WeakMap 实例,WeakSet 实例的用处没有那么大。不过,弱集合在给对象打标签时还是有价值的。
来看下面的例子,这里使用了一个普通 Set:
const disabledElements = new Set()
const loginButton = document.querySelector('#login')
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton)这样,通过查询元素在不在 disabledElements 中,就可以知道它是不是被禁用了。不过,假如元素从 DOM 树中被删除了,它的引用却仍然保存在 Set 中,因此垃圾回收程序也不能回收它。
为了让垃圾回收程序回收元素的内存,可以在这里使用 WeakSet:
const disabledElements = new WeakSet()
const loginButton = document.querySelector('#login')
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton)这样,只要 WeakSet 中任何元素从 DOM 树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存(假设没有其他地方引用这个对象)。
Set、Map、WeakSet、WeakMap
Set对象可以存储任何类型的数据。值是唯一的,没有重复的值。
Map对象保存键值对,任意值都可以成为它的键或值。
WeakSet 结构与 Set 类似,也是不重复的值的集合
WeakMap 对象是一组键值对的集合
不同:
WeakSet 的成员只能是对象,而不能是其他类型的值。WeakSet 不可遍历。
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
WeakMap的键名所指向的对象,不计入垃圾回收机制。
6.8 迭代与扩展操作
ECMAScript 6 新增的迭代器和扩展操作符对集合引用类型特别有用。这些新特性让集合类型之间相互操作、复制和修改变得异常方便。
如本章前面所示,有 4 种原生集合类型定义了默认迭代器:
❑ Array
❑ 所有定型数组
❑ Map
❑ Set
很简单,这意味着上述所有类型都支持顺序迭代,都可以传入 for-of 循环
这也意味着所有这些类型都兼容扩展操作符。扩展操作符在对可迭代对象执行浅复制时特别有用,只需简单的语法就可以复制整个对象。
对于期待可迭代对象的构造函数,只要传入一个可迭代对象就可以实现复制。
let map1 = new Map([
[1, 2],
[3, 4]
])
let map2 = new Map(map1)
console.log(map1) // Map { 1: 2, 3: 4 }
console.log(map2) // Map { 1: 2, 3: 4 }当然,也可以构建数组的部分元素:
let arr1 = [1, 2, 3]
let arr2 = [0, ...arr1, 4, 5]
console.log(arr2) // [ 0, 1, 2, 3, 4, 5 ]浅复制意味着只会复制对象引用:
let arr1 = [{}]
let arr2 = [...arr1]
arr1[0].foo = 'bar'
console.log(arr2[0]) // { foo: 'bar' }上面的这些类型都支持多种构建方法,比如 Array.of()和 Array.from()静态方法。在与扩展操作符一起使用时,可以非常方便地实现互操作:
let arr1 = [1, 2, 3]
// 把数组复制到定型数组
let typedArr1 = Int16Array.of(...arr1)
let typedArr2 = Int16Array.from(arr1)
console.log(typedArr1) // Int16Array { 0: 1, 1: 2, 2: 3 }
console.log(typedArr2) // Int16Array { 0: 1, 1: 2, 2: 3 }
// 把数组复制到映射
let map = new Map(arr1.map((x) => [x, 'val' + x]))
console.log(map) // Map { 1: 'val1', 2: 'val2', 3: 'val3' }
// 把数组复制到集合
let set = new Set(typedArr2)
console.log(set) // Set { 0: 1, 1: 2, 2: 3 }
// 把集合复制回数组
let arr2 = [...set]
console.log(arr2) // [ 1, 2, 3 ]